//
// Graphics.cpp
//

#include "Graphics.h"
#include "ShaderEffect.h"

#define FPS_FONT_NAME		L"FIXEDSYS"
#define DEBUG_FIXED_WIDTH	1280
#define DEBUG_FIXED_HEIGHT	720

CGraphics::CGraphics(void)
	: _pDirect3D(NULL), _pDevice(NULL), _MsaaLevel(D3DMULTISAMPLE_NONE), _hTarget(NULL), _pEffect(NULL),
	  _uWidth(0), _uHeight(0), _uRefresh(0), _pFont(NULL), _bHardwareAccel(false), _dwMsaaQuality(0)
{
	LOG_WRITE(DS_GRAPHICS_CREATED);
	ZeroMemory(&_PresentParams, sizeof(D3DPRESENT_PARAMETERS));
}

CGraphics::~CGraphics(void)
{
	LOG_WRITE(DS_GRAPHICS_DESTROYED);
	release();
}

bool CGraphics::init(HWND hWnd, UINT uWidth, UINT uHeight, bool isWindowed)
{
	if(_pDirect3D) {
		LOG_WRITE(DS_GRAP_INITIALIZED_TWICE);
		return false;
	}

	try {
		// Initialize Direct3D
		_pDirect3D = Direct3DCreate9(D3D_SDK_VERSION);
		if(_pDirect3D == NULL) {
			LOG_WRITE(DS_D3D_CREATE_FAILED, D3D_SDK_VERSION);
			throw std::exception(ES_D3D_CREATE_FAILED);
		} else {
			LOG_WRITE(DS_D3D_CREATED, D3D_SDK_VERSION);
		}

		// Check hardware compatibility
		_hTarget = hWnd;
		if(!_checkPrerequisite(uWidth, uHeight, isWindowed)) {
			LOG_WRITE(DS_GRAP_PREREQUISITE_FALIED);
			throw std::exception(ES_HARDWARE_UNSATISFIED);
		} else {
			LOG_WRITE(DS_GRAP_PREREQUISITE_PASSED);
		}
		::SetWindowPos(hWnd, NULL, 0, 0, _uWidth, _uHeight, 0);
	
		// Set present parameters
		_PresentParams.AutoDepthStencilFormat	= D3DFMT_D16;
		_PresentParams.BackBufferCount			= 1;
		_PresentParams.BackBufferFormat			= D3DFMT_X8R8G8B8;
		_PresentParams.BackBufferWidth			= _uWidth;
		_PresentParams.BackBufferHeight			= _uHeight;
		_PresentParams.EnableAutoDepthStencil	= true;

#ifdef _DEBUG
		// Debug in windowed mode.
		_PresentParams.FullScreen_RefreshRateInHz = 0;
		_PresentParams.Windowed = TRUE;
#else
		// Release in fullscreen.
		if(bWindowed) {
			_PresentParams.FullScreen_RefreshRateInHz = 0;
			_PresentParams.Windowed = TRUE;
		} else {
			_PresentParams.FullScreen_RefreshRateInHz = _uRefresh;
			_PresentParams.Windowed = FALSE;
		}
#endif // _DEBUG

		_PresentParams.hDeviceWindow			= _hTarget;
		_PresentParams.MultiSampleType			= _MsaaLevel;
		_PresentParams.MultiSampleQuality		= _dwMsaaQuality;
		_PresentParams.SwapEffect				= D3DSWAPEFFECT_DISCARD;
		_PresentParams.PresentationInterval		= D3DPRESENT_INTERVAL_IMMEDIATE;

		// Create Direct3D device.
		HRESULT hr;
		hr = _pDirect3D->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, _hTarget, 
			_bHardwareAccel ? D3DCREATE_HARDWARE_VERTEXPROCESSING : D3DCREATE_SOFTWARE_VERTEXPROCESSING,
			&_PresentParams, &_pDevice);
		if(FAILED(hr)) {
			LOG_WRITE(DS_CREATE_DEVICE_FAILED, hr);
			throw std::exception(ES_DEVICE_CREATE_FAILED);
		}
	} catch (std::exception &e) {
		MessageBoxA(NULL, e.what(), "Error", MB_ICONERROR);
		ReleaseDevice(_pDevice);
		ReleaseDirect3D(_pDirect3D);
		ReleaseFont(_pFont);
		return false;
	}
	
	::D3DXMatrixIdentity(&_matView);
	setAlign(RenderAlign_Center);

	_setRenderParameters();
	LOG_WRITE(DS_GRAPHICS_INITIALIZED);
	return true;
}

bool CGraphics::_checkPrerequisite(UINT uWidth, UINT uHeight, bool isWindowed)
{
	// Set target resolution.
#ifdef _DEBUG
	_uWidth = DEBUG_FIXED_WIDTH;
	_uHeight = DEBUG_FIXED_HEIGHT;
	_uRefresh = 60;
#else
	if(uWidth == 0) {
		_uWidth = GetSystemMetrics(SM_CXSCREEN);
		_uHeight = GetSystemMetrics(SM_CYSCREEN);
	} else {
		_uWidth = uWidth;
		_uHeight = uHeight;
	}
	_uRefresh = 60;
#endif // _DEBUG
	LOG_WRITE(DS_GRAP_TARGET_RESOLUTION, _uWidth, _uHeight, _uRefresh);
	
	// Check backbuffer format compatibility.
	HRESULT hr;
	hr = _pDirect3D->CheckDeviceType(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL,
		D3DFMT_X8R8G8B8, D3DFMT_X8R8G8B8, FALSE);
	if(FAILED(hr)) {
		LOG_WRITE(DS_CHECK_DEVTYPE_FAILED, hr);
		return false;
	}

	// Enumerate and verify display modes.
	D3DDISPLAYMODE mode;
	int count, i;

	count = _pDirect3D->GetAdapterModeCount(D3DADAPTER_DEFAULT, D3DFMT_X8R8G8B8);
	if(count == 0) {
		LOG_WRITE(DS_NO_DISPLAY_MODE);
		return false;
	} else {
		LOG_WRITE(DS_DISPLAY_MODE_COUNT, count);
	}
	for(i = 0; i < count; i++) {
		hr = _pDirect3D->EnumAdapterModes(D3DADAPTER_DEFAULT, D3DFMT_X8R8G8B8, i, &mode);
		if(FAILED(hr)) {
			LOG_WRITE(DS_ENUM_ADAPTER_MODE_FAILED, hr);
			return false;
		} else {
			LOG_WRITE(DS_DISPLAY_MODE_FORMAT, i + 1, mode.Width, mode.Height, mode.RefreshRate);
			if(mode.Width == _uWidth && mode.Height == _uHeight && mode.RefreshRate == _uRefresh) {
				LOG_WRITE(DS_TARGET_DISPMODE_FOUND);
				break;
			}
		}
	}
	if(i >= count) {
		LOG_WRITE(DS_NO_SUITABLE_DISPMODE);
		return false;
	}

	D3DCAPS9 caps;
	hr = _pDirect3D->GetDeviceCaps(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, &caps);
	if(FAILED(hr)) {
		LOG_WRITE(DS_GET_DEVICE_CAPS_FAILED, hr);
		return false;
	}

	// Check hardware vertex processing.
	if(caps.VertexProcessingCaps) {
		_bHardwareAccel = true;
		LOG_WRITE(DS_HARDWARE_ACCEL_ENABLED);
	} else {
		_bHardwareAccel = false;
		LOG_WRITE(DS_HARDWARE_ACCEL_DISABLED);
	}

	// Check pixel shader compatibility.
	if(caps.PixelShaderVersion < D3DPS_VERSION(2, 0)) {
		LOG_WRITE(DS_PIXEL_SHADER_UNSUPPORTED);
		return false;
	} else {
		LOG_WRITE(DS_PIXEL_SHADER_SUPPORTED);
	}

	// Check MSAA compatibility.
	DWORD dwBestQuality;
	_MsaaLevel = D3DMULTISAMPLE_2_SAMPLES;
	hr = _pDirect3D->CheckDeviceMultiSampleType(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, D3DFMT_X8R8G8B8, FALSE,
		_MsaaLevel, &dwBestQuality);
	if(FAILED(hr)) {
		LOG_WRITE(DS_MSAA_DISABLED);
		_MsaaLevel = D3DMULTISAMPLE_NONE;
		_dwMsaaQuality = 0;
	} else {
		if(dwBestQuality > 2) _dwMsaaQuality = 2;
		else _dwMsaaQuality = dwBestQuality - 1;
		LOG_WRITE(DS_MSAA_ENABLED, _dwMsaaQuality);
	}

	return true;
}

void CGraphics::_setRenderParameters(void)
{
	_pDevice->SetTransform(D3DTS_VIEW, &_matView);
	_pDevice->SetTransform(D3DTS_PROJECTION, &_matProjection);

	_pDevice->SetFVF(Vertex::_fvf);
	_pDevice->SetRenderState(D3DRS_LIGHTING, FALSE);
	_pDevice->SetRenderState(D3DRS_CULLMODE, D3DCULL_NONE);
	_pDevice->SetRenderState(D3DRS_MULTISAMPLEANTIALIAS, TRUE);
	_pDevice->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);
	_pDevice->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);

	if(_pEffect)
		applyShaderEffect(_pEffect);

	ReleaseFont(_pFont);
	if(!createFont(&_pFont, FPS_FONT_NAME, 28, true, false)) {
		LOG_WRITE(DS_GRAP_CREATE_FONT_FAILED);
	}
}

void CGraphics::associateTexture(LPDIRECT3DTEXTURE9 pTexture, UINT uStage)
{
	_pDevice->SetTexture(uStage, pTexture);
}

void CGraphics::initializeTextureStage(UINT uStage)
{
	_pDevice->SetSamplerState(uStage, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR);
	_pDevice->SetSamplerState(uStage, D3DSAMP_MINFILTER, D3DTEXF_LINEAR);
	_pDevice->SetSamplerState(uStage, D3DSAMP_MIPFILTER, D3DTEXF_LINEAR);
}

void CGraphics::setAlign(RenderAlign align)
{
	switch(align)
	{
	case RenderAlign_LeftTop:
		::D3DXMatrixOrthoOffCenterLH(&_matProjection, 0.0f, (float)_uWidth, 
			-(float)_uHeight, 0.0f, 0.1f, 600.0f);
		break;
	case RenderAlign_Center:
		::D3DXMatrixOrthoOffCenterLH(&_matProjection, -(float)_uWidth / 2, (float)_uWidth / 2,
			-(float)_uHeight / 2, (float)_uHeight / 2, 0.1f, 600.0f);
		break;
	}
	_pDevice->SetTransform(D3DTS_PROJECTION, &_matProjection);
}

void CGraphics::transformWorld(const D3DXMATRIX &matWorld)
{
	_pDevice->SetTransform(D3DTS_WORLD, &matWorld);
}

bool CGraphics::preRender(void)
{
	HRESULT hr;
	if(_pDevice) {
		hr = _pDevice->Present(NULL, NULL, NULL, NULL);
		if(FAILED(hr)) {
			LOG_WRITE(DS_PRESENT_FAILED, hr);
			return false;
		}
		hr = _pDevice->BeginScene();
		if(FAILED(hr)) {
			LOG_WRITE(DS_BEGIN_SCENE_FAILED, hr);
			return false;
		}
	}
	return true;
}

void CGraphics::postRender(void)
{
	if(_pDevice) {
		_drawFps();
		_pDevice->EndScene();
	}
}

void CGraphics::release(void)
{
	ReleaseFont(_pFont);
	ReleaseDevice(_pDevice);
	ReleaseDirect3D(_pDirect3D);
	LOG_WRITE(DS_GRAPHICS_RELEASED);
}

void CGraphics::clearScene(D3DCOLOR Color)
{
	_pDevice->Clear(0, NULL,
		D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER,
		Color, 1.0f, 0);
}

void CGraphics::_drawFps(void)
{
	static UINT frame_count = 0;
	static UINT last = GetTickCount();
	static UINT fps;
	RECT rc = {0, 0, 400, 30};
	
	unsigned int interval = GetTickCount() - last;
	wchar_t buffer[10];
	frame_count++;
	if(interval > 1024) {
		fps = UINT((float)(frame_count << 10) / (float)interval);
		last = GetTickCount();
		frame_count = 0;
	}
	_pFont->DrawText(NULL, ::_itow(fps, buffer, 10), -1, &rc, DT_VCENTER, 0xFFFF0000);
}

bool CGraphics::createFont(LPD3DXFONT *ppFont, const wchar_t *pwszFontName, UINT uSize, bool bBold, bool bItalic)
{
	HRESULT hr = ::D3DXCreateFont(_pDevice,
			uSize, 0, bBold ? FW_BOLD : FW_NORMAL, 0, bItalic ? TRUE : FALSE,
			DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, DEFAULT_QUALITY , DEFAULT_PITCH | FF_DONTCARE,
			pwszFontName, ppFont);
	if(FAILED(hr)) {
		LOG_WRITE(DS_D3D_CREATE_FONT_FAILED, hr);
		return false;
	}
	return true;
}

bool CGraphics::createTextureFromFile(const wchar_t *pwszFileName, LPDIRECT3DTEXTURE9 *ppTexture, bool isNonPowerOfTwo)
{
	if(pwszFileName == NULL || ppTexture == NULL) return false;
	HRESULT hr = D3DXCreateTextureFromFileEx(_pDevice, pwszFileName,
		isNonPowerOfTwo ? D3DX_DEFAULT_NONPOW2 : 0,
		isNonPowerOfTwo ? D3DX_DEFAULT_NONPOW2 : 0,
		D3DX_DEFAULT, 0, D3DFMT_FROM_FILE, D3DPOOL_MANAGED,
		D3DX_DEFAULT, D3DX_DEFAULT, 0, NULL, NULL, ppTexture);
	if(FAILED(hr)) {
		LOG_WRITE(DS_TEX_FROM_FILE_EX_FAILED, hr);
		return false;
	}
	return true;
}

bool CGraphics::createTextureFromMemory(void *pData, size_t uSize, LPDIRECT3DTEXTURE9 *ppTexture, bool isNonPowerOfTwo)
{
	if(pData == NULL || ppTexture == NULL) return false;
	HRESULT hr = D3DXCreateTextureFromFileInMemoryEx(_pDevice, pData, uSize,
		isNonPowerOfTwo ? D3DX_DEFAULT_NONPOW2 : 0,
		isNonPowerOfTwo ? D3DX_DEFAULT_NONPOW2 : 0,
		D3DX_DEFAULT, D3DX_DEFAULT, D3DFMT_FROM_FILE,
		D3DPOOL_MANAGED, D3DX_DEFAULT, D3DX_DEFAULT, 0,
		NULL, NULL, ppTexture);
	if(FAILED(hr)) {
		LOG_WRITE(DS_TEX_FROM_MEM_EX_FAILED, hr);
		return false;
	}
	return true;
}

bool CGraphics::createVertexBuffer(UINT uSize, LPDIRECT3DVERTEXBUFFER9 *ppVertexBuffer, bool isWriteOnly)
{
	HRESULT hr = _pDevice->CreateVertexBuffer(uSize * sizeof(Vertex), 
		isWriteOnly ? D3DUSAGE_WRITEONLY : 0, Vertex::_fvf, D3DPOOL_MANAGED, ppVertexBuffer, NULL);
	if(FAILED(hr)) {
		LOG_WRITE(DS_VERTEX_BUF_CREATE_FAILED, hr);
		return false;
	}
	return true;
}

void CGraphics::drawVertexBuffer(LPDIRECT3DVERTEXBUFFER9 pVertexBuffer, UINT uOffset, UINT uCount, D3DPRIMITIVETYPE type)
{
	_pDevice->SetStreamSource(0, pVertexBuffer, uOffset * sizeof(Vertex), sizeof(Vertex));
	_pDevice->DrawPrimitive(type, 0, uCount / 3);
}

void CGraphics::makeBillboard(LPDIRECT3DVERTEXBUFFER9 pVertexBuffer, float halfWidth, float halfHeight)
{
	Vertex *v;
	pVertexBuffer->Lock(0, 0, (void **)&v, 0);
	v[0].set(-halfWidth, halfHeight, 20.0f, 0.0f, 0.0f);
	v[1].set(halfWidth, halfHeight, 20.0f, 1.0f, 0.0f);
	v[2].set(-halfWidth, -halfHeight, 20.0f, 0.0f, 1.0f);
	v[3].set(halfWidth, halfHeight, 20.0f, 1.0f, 0.0f);
	v[4].set(halfWidth, -halfHeight, 20.0f, 1.0f, 1.0f);
	v[5].set(-halfWidth, -halfHeight, 20.0f, 0.0f, 1.0f);
	pVertexBuffer->Unlock();
}

void CGraphics::makeBox(LPDIRECT3DVERTEXBUFFER9 pVertexBuffer, float halfWidth, float halfHeight, float halfDepth)
{
	Vertex *v;
	pVertexBuffer->Lock(0, 0, (void **)&v, 0);
	const float sign[6][2] = {{-1.0f, 1.0f}, {1.0f, 1.0f}, {-1.0f, -1.0f}, {1.0f, 1.0f}, {1.0f, -1.0f}, {-1.0f, -1.0f}};
	const float texture[6][2] = {{0.0f, 0.0f}, {1.0f, 0.0f}, {0.0f, 1.0f}, {1.0f, 0.0f}, {1.0f, 1.0f}, {0.0f, 1.0f}};
	// front
	for(int i = 0; i < 6; i++) {
		v[i].set(sign[i][0] * halfWidth, sign[i%6][1] * halfHeight, -halfDepth, texture[i][0], texture[i][1]);
	}
	// left
	for(int i = 0; i < 6; i++) {
		v[i + 6].set(-halfWidth, sign[i][0] * halfHeight, sign[i][1] * halfDepth, texture[i][0], texture[i][1]);
	}
	// right
	for(int i = 0; i < 6; i++) {
		v[i + 12].set(halfWidth, sign[i][0] * halfHeight, sign[i][1] * halfDepth, texture[i][0], texture[i][1]);
	}
	// back
	for(int i = 0; i < 6; i++) {
		v[i + 18].set(sign[i][0] * halfWidth, sign[i][1] * halfHeight, halfDepth, texture[i][0], texture[i][1]);
	}
	// top
	for(int i = 0; i < 6; i++) {
		v[i + 24].set(sign[i][0] * halfWidth, halfHeight, sign[i][1] * halfDepth, texture[i][0], texture[i][1]);
	}
	// bottom
	for(int i = 0; i < 6; i++) {
		v[i + 30].set(sign[i][0] * halfWidth, -halfHeight, sign[i][1] * halfDepth, texture[i][0], texture[i][1]);
	}
	pVertexBuffer->Unlock();
}

void CGraphics::resetDevice(void)
{
	if(_pDevice) {
		_pDevice->Reset(&_PresentParams);
		_setRenderParameters();
	}
}

void CGraphics::applyShaderEffect(CShaderEffect *pEffect)
{
	if(pEffect == NULL) {
		_pEffect = NULL;
		return ;
	}

	HRESULT hr;
	switch(pEffect->_type)
	{
	case CShaderEffect::ShaderType_PixelShader:
		hr = _pDevice->SetPixelShader(pEffect->_pPixelShader);
		break;
	case CShaderEffect::ShaderType_VertexShader:
		hr = _pDevice->SetVertexShader(pEffect->_pVertexShader);
		break;
	default:
		_pEffect = NULL;
		return ;
	}
	if(FAILED(hr)) {
		LOG_WRITE(DS_SHADER_APPLY_FAILED, hr);
	}
}
